Este projeto final tem como objetivo explorar os conhecimentos adquiridos nas aulas práticas. Por meio uma trilha guiada para construir uma aplicação que tem por objetivo analisar imagens e extrair uma série de informações que serão utilizadas para compor uma análise de imagens e vídeos afim de construir uma forma de auditoria automatizada baseado em modelos de inteligência artificial.
Este projeto poderá ser feita por grupos de até 4 pessoas.
| Nome dos Integrantes | RM | Turma |
|---|---|---|
| Integrante 1 | RM 12345 | 1IA |
| Integrante 2 | RM 12345 | 1IA |
| Integrante 3 | RM 12345 | 1IA |
| Integrante 4 | RM 12345 | 1IA |
Por ser um projeto guiado, fique atento quando houver as marcações Implementação indica que é necessário realizar alguma implementação em Python no bloco a seguir onde há a inscrição ## IMPLEMENTAR e Resposta indica que é esperado uma resposta objetiva relacionado a algum questionamento.
Cada grupo pode utilizar nas respostas objetivas quaisquer itens necessários que enriqueçam seu ponto vista, como gráficos, fotos e, até mesmo, trechos de código-fonte.
Pode-se utilizar quantos blocos forem necessários para realizar determinadas implementações ou utilizá-las para justificar as respostas. Não é obrigatório utilizar somente o bloco indicado.
Ao final não se esqueça de subir os arquivos do projeto nas contas do GitHub de cada membro, ou subir na do representante do grupo e os membros realizarem o fork do projeto.
A avaliação terá mais ênfase nos seguintes tópicos de desenvolvimento do projeto:
Para cada item haverá uma métrica de sucesso a ser perseguida e recomendações de como atingi-la. De todo modo os grupos terão liberdade para propor abordagem ligeiramente diferentes para atinger as mesmas métricas. Apenas fica vetado o uso de APIs baseadas em cloud.
Dúvidas: clique aqui para perguntar e abrir uma issue.
Requisitos mínimos para execução deste projeto:
Para verificar se possui os pacotes obrigatórios execute os comandos abaixo de importação.
import numpy as np
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import seaborn as sns
import cv2
#Exibição na mesma tela do Jupyter
%matplotlib inline
from os import listdir
from os.path import isfile, join
import pandas as pd
import dlib
from keras.models import Sequential, load_model
from keras.layers.core import Dense, Dropout, Activation
import keras
from keras.models import Sequential, Model
from keras.layers import Dense, Conv2D, Dropout, Flatten, MaxPooling2D, ZeroPadding2D, Convolution2D
from keras.preprocessing import image
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
import ast
from sklearn.model_selection import train_test_split
import os
from datetime import datetime
plt.style.use('seaborn')
sns.set_style("whitegrid", {'axes.grid' : False})
Importante ressaltar que precisamos do OpenCV >= 3.4.2.
Execute o comando a baixo para verificar. Se obter versão menor que 3.4.2, instale a versão correta e depois prossiga com o projeto.
print(cv2.__version__)
Atualmente, com a tecnologia disponível e acessível, sobretudo na obtenção de imagens e vídeos e em seu processamento, permite apoiar com mais ênfase atividades de auditoria e investigação de forma automatizada, sem a necessidade de uma pessoa realizar tais análises e ainda com maior acurácia.
O uso de tecnologias relacionadas a visão computacional pode contribuir para tornar mais eficiente investigações de pessoas e objetos baseadas em suas características e em seu perfil, além de tornar esta atividade mais rápida e completa.
A proposta deste projeto é apoiar uma aplicação de auditoria e investigação para automatizar a busca por regiões de interesse (objetos e determinadas pessoas) com as seguintes finalidades:
Uma busca manual em vídeos de vigilância é bem onerosa e pode deixar passar evidências importantes em processos investigativos.
Ainda assim, a análise humana é realizada em último caso para decidir, se dentre as evidências coletadas, quais devem seguir para investigação mais apurada e quais não. A tarefa humana é mais de validação do que exploração.
Para alcançar este objetivo iremos construir uma aplicação capaz de analisar um vídeo específico de um escritórioo, baseado na série de TV The Office. A partir dele e de modelos de classificação de imagens, iremos coletar e armazenar imagens das regiões de interesse citadas para posterior análise de investigações.
Deste modo, conforme já apresentado, seguiremos com o seguinte roteiro:
Por fim, deverá se realizado uma conclusão deste estudo, apresentando como foi a realização deste processo, pontos de acerto, pontos de melhoria e como poderia ser feito para aperfeiçoar os resultados.
Nesta primeira parte, iremos construir um modelo baseado em redes neurais profundas (deep learning) capaz de identificar, a partir de uma imagem, qual é a idade da pessoa.
Este tipo de classificador requer um mapeamento mais profundo de cada imagem além de ser necessário um número considerável de imagens para cada idade ou faixa de idade.
Devido a necessidade de um número alto de imagens, vamos utilizar o dataset IMDB-WIKI – 500k+ face images with age and gender labels que foi utilizado no desafio DEX: Deep EXpectation of apparent age from a single image.
Também foi utilizado dois excelentes artigos de Sefik Ilkin Serengil, que podem ser acessados aqui e aqui. Esses artigos demonstram a aplicação do VGGFace para as tarefas de reconhecimento de idade e gênero. Artigos que foram ligeiramente adaptados e utilizados para o desafio.
As imagens estão disponíveis na pasta imagens.
O arquivo age-faces-dataset.csv, na pasta csv possui a relação de cada sujeito, contendo sua idade, localização da face, idade e referência da imagem, gênero, dentre outros campos. Com esta referência é possível associar determinado sujeito com sua face.
No conjunto de dados, a representação do gênero masculino é codificada com o valor 1 e o gênero feminino com o valor 0.
Abra o conjunto de dados utilizando o Pandas. Utilize o método read_csv.
# IMPLEMENTAR
df = pd.read_csv('csv/age-faces-dataset.csv')
Execute o comando abaixo para apresentação de uma amostra do conjunto de dados.
df = df.tail(2)
Antes de começarmos, avalie o conjunto de dados Pandas e verifique se existe alguma otimização. Sugerimos verificar e filtrar somente sujeitos com idade maior do que 0 e menor do que 100.
Analise o histograma e investigue com filtros do Pandas para avaliar se existe necessidade de realização de algum data cleasing no dataset, de acordo com a sugestão pedida.
Havendo necessidade de data cleasing realize os devidos ajustes.
Vamos investigar a distribuição de idades pelo histograma. Execute o comando abaixo.
histograma_idade = df['age'].hist(bins=df['age'].nunique())
De acordo com os dados de idade, vamos buscar a existência de idades inválidas. A começar com idades menores do que 0.
df[df['age'] < 0]
Agora idades maiores do que 100.
df[df['age'] > 100]
Diante dos dados apresentados, realize a limpeza dos registros com idades inválidas ou de não interesse do projeto. Para realizar a limpeza utilize da seguinte forma:
df = df[df['age'] <= idade_limite_superior]
df = df[df['age'] > idade_limite_inferior]
Agora implemente a limpeza dos dados.
df = df[df['age'] <= 100]
df = df[df['age'] > 0]
Vamos executar novamente os comandos abaixo para ter certeza que os dados foram limpos.
len(df[df['age'] > 100]), len(df[df['age'] < 0])
Se o resultado obtido foi 0 para ambos os filtros (exemplo (0,0)) signfica que estamos prontos para avançar.
A técnica de transfer learning é particularmente útil ao combinar modelos já validados em aplicações mais robustas, resultado de competições de grande porte.
Um benchmark nesse campo aplicado a faces é o VGGFace do grupo VGG, o Visual Geometry Group da Univerdade de Oxford.
A arquitetura proposta pelo VGGFace é capaz de classificar faces com precisão acima de 90% em alguns trabalhos, como por exemplo Zhang, Lingfeng & Kakadiaris, Ioannis. (2017). Local classifier chains for deep face recognition. 158-167.
Este modelo tem a seguinte arquitetura.
Observando a primeira camada, precisamos portanto padronizar as imagens de treinamento no tamanho 224 x 224 (comprimento x largura).
Os grupos que decidirem criar modelos alternativos devem explicar qual arquitetura utiliza e seus motivadores.
O algoritmo abaixo normaliza as imagens para o tamanho de entrada do modelo VGGFace e também converte o valor em intenside de pixel de 0 a 255 para 0 a 1, com valores em pontos flutuantes.
Valores em ponto flutuante são melhores para se trabalhar com convergência de modelos durante os cálculos entre as camadas das redes neurais.
import time
tamanho_imagem = (224, 224)
faces = []
for index, row in df.iterrows():
image_face = image.load_img("imagens/%s" % ast.literal_eval(row["full_path"])[0], grayscale=False, target_size=tamanho_imagem)
image_array = image.img_to_array(image_face).reshape(1, -1)[0]
image_array /= 255
faces.append(image_array)
Verifique a quantidade de imagens de faces obtidas pelo conjunto de dados.
# IMPLEMENTAR
qtd_faces = len(faces)
print("O total de faces de imagens é de " + str(qtd_faces) + ".")
O próximo passo é a definição do número de classes. Como a definição de classes é zero based sempre defina adicionando 1 ao valor final.
Exemplo, se o número de classes compreender entre idades maior do que 0 e menor ou igual a 100, teremos 100 classes, logo o número de classes de idade é 101.
# IMPLEMENTAR
classes_idade = 101
Uma forma de categorizarmos as classes é utilizando o modelo one hot encode. Onde será expressado em um array com todas as classes. A classe correspondente a um determindo registro será armazenado como o valor 1 naquela classe.
idades = df['age'].values
idades_classes = keras.utils.to_categorical(idades, classes_idade)
Faça um teste do one hot encoding. Exiba os dados do registro de índice zero do conjunto de dados.
idades_classes[0]
Em seguida, vamos adaptar o shape dos dados no padrão que o framework Keras utiliza.
features_imagem = []
features_imagem = np.array(faces)
features_imagem = features_imagem.reshape(features_imagem.shape[0], 224, 224, 3)
Para que o modelo seja robusto, é importante validar um subconjunto de amostras que não participaram do treinamento inicial.
Defina uma porcentagem, da base total, que deverá ser separa somente para a validação. Expresse o número na forma de número fracionário, por exemplo 40% equivale a 0.4.
# IMPLEMENTAR
porcentagem_validacao = 0.3
Execute o método train_test_split para realizar a divisão de cada tipo de amostra, sendo:
treinamento_x contém os features de treinamentoteste_x contém os features de validação (também é chamado de teste)treinamento_y contém as classes de treinamentoteste_y contém as classes de validaçãotreinamento_x, teste_x, treinamento_y, teste_y = train_test_split(features_imagem,
idades_classes,
test_size=porcentagem_validacao)
O modelo VGGFace ainda não está por padrão na biblioteca Keras. Mesmo assim, como o artigo citado anteriormente possui a arquitetura definida, podemos implementá-la manualmente.
#VGG-Face model
modelo = Sequential()
modelo.add(ZeroPadding2D((1,1),input_shape=(224,224, 3)))
modelo.add(Convolution2D(64, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(64, (3, 3), activation='relu'))
modelo.add(MaxPooling2D((2,2), strides=(2,2)))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(128, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(128, (3, 3), activation='relu'))
modelo.add(MaxPooling2D((2,2), strides=(2,2)))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(256, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(256, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(256, (3, 3), activation='relu'))
modelo.add(MaxPooling2D((2,2), strides=(2,2)))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(512, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(512, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(512, (3, 3), activation='relu'))
modelo.add(MaxPooling2D((2,2), strides=(2,2)))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(512, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(512, (3, 3), activation='relu'))
modelo.add(ZeroPadding2D((1,1)))
modelo.add(Convolution2D(512, (3, 3), activation='relu'))
modelo.add(MaxPooling2D((2,2), strides=(2,2)))
modelo.add(Convolution2D(4096, (7, 7), activation='relu'))
modelo.add(Dropout(0.5))
modelo.add(Convolution2D(4096, (1, 1), activation='relu'))
modelo.add(Dropout(0.5))
modelo.add(Convolution2D(2622, (1, 1)))
modelo.add(Flatten())
modelo.add(Activation('softmax'))
modelo.load_weights("pesos/vgg_face_weights.h5")
Assim como em qualquer modelo de transfer learning, precisamos adicionar na última camada as classes correspondentes. Deste modo, na saída da camada convulacional precisamos definir o número de classes. É o primeiro parâmetro do objeto Convolution2D.
for layer in modelo.layers[:-7]:
layer.trainable = False
saida_modelo = Sequential()
# IMPLEMENTAR
saida_modelo = Convolution2D(classes_idade, (1, 1), name='predictions')(modelo.layers[-4].output)
saida_modelo = Flatten()(saida_modelo)
saida_modelo = Activation('softmax')(saida_modelo)
modelo_idade = Model(inputs=modelo.input, outputs=saida_modelo)
Este tipo de treinamento possui muitas imagens. A etapa de treinamento pode demorar até mesmo mais do que 2 horas dependendo do tipo de computador utilizado.
Apesar da demora, este desafio traz um desafio real de treinar um modelo de imagens com quantidade de exemplos adequado para as classificações requeridas.
Sugiro que após o treinamento ser realizado, salvar os novos pesos e a rede para utilizar mais adiante nas inferências.
Revise a arquitetura da rede com o comando a seguir.
modelo_idade.summary()
O número de épocas é responsável por quantas vezes o modelo percorrerá o ciclo de foward e back propagation.
Como este modelo possui muitas imagens, sugerimos um valor mínimo de 2 épocas.
# IMPLEMENTAR
numero_epocas = 2
Caso o modelo trave no treinamento, experimente diminuir o batch_size.
modelo_idade.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
historico = modelo_idade.fit(x=treinamento_x, y=treinamento_y,
validation_data=(teste_x, teste_y), epochs=numero_epocas, batch_size=32)
Armazene os valores do modelo e dos pesos para inferências futuras.
Os valores de erro e acurácia são importantes indicadores de desempenho da rede. Avaliar a tendência de subida ou descida destes parâmetros é essencial para localizar, por exemplo, prooblemas de overfitting e underfitting. Com o suporte dos gráficos avalie a tendência do modelo ao logo das épocas.
# Para deixar no formato do Seaborn os gráficos do Pyplot
sns.set()
# Exibindo dados de Acurácia/Precisão
plt.plot(historico.history['acc'])
plt.plot(historico.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# Exibindo dados de Perda
plt.plot(historico.history['loss'])
plt.plot(historico.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
Pergunta: Qual foi a tendência da função de erro e acurácia do modelo?
Resposta: Não foi possivel avaliar, pois o algoritmo foi executado com apenas 2 ou 5 imagens por limitação de hardware.
Os comandos abaixo são de referência para carregar um modelo para inferências futuras.
# carregando o modelo no formato HDf5
modelo_idade = load_model("modelos/modelo_idade.h5")
modelo_idade.load_weights("pesos/modelo_idade_pesos.h5")
Este classificador tem uma particularidade diferente quando comparado com outros classificadores. Geralmente um classificador retorna, dado uma amostra, qual a classe mais próxima ou qual tem maior probabilidade estatística. Por exemplo, se fóssemos classificar um objeto, o retorno seria a classe correspondente àquele objeto com maior semelhança, mutualmente excludente, sendo uma única classe como resultado.
Neste caso é diferente.
Cada classe se refere a uma idade, logo, ao invés de nos basearmos na classe dominante, com maior probabilidade, podemos somar as probabilidades com cada classe e assim ter um valor de idade mais aproximado (veja na imagem abaixo do pipeline, item 5).
É por essa razão que a acurácia do modelo, baseada somente na classe dominante é baixa.
Os comandos abaixo contabilizam a predição baseada na soma das probabilidades de cada classe.
predicoes_idade = modelo_idade.predict(teste_x)
classes_idade_saida = np.array([i for i in range(0, classes_idade)])
predicoes_agrupadas = np.sum(predicoes_idade * classes_idade_saida, axis = 1)
Deste modo, o valor da acurácia não é o melhor para avaliarmoso modelo. A uma outra forma de avaliação é utilizarmos o Erro Médio Absoluto.
O uso do Erro Médio Absoluto foi integralmente aplicado de acordo com este artigo.
erro_medio_absoluto = 0
atual_media = 0
for i in range(0 ,predicoes_agrupadas.shape[0]):
predicao = int(predicoes_agrupadas[i])
atual = np.argmax(teste_y[i])
erro_abs = abs(predicao - atual)
atual_media = atual_media + atual
erro_medio_absoluto += erro_abs
erro_medio_absoluto /= predicoes_agrupadas.shape[0]
print("Erro médio absoluto (+/-): ", erro_medio_absoluto, " anos")
print("Exemplos analisados: ",predicoes_agrupadas.shape[0])
Critério de sucesso: com base nas amostras de teste, o erro absoluto precisa ser não mais do que (+/-) 8.
Vamos analisar um conjunto de 6 imagens e verificar a coerência do modelo.
Antes precisamos contruir uma função, a carregarImagem para padronizar a imagem, redimensionando para o formato do modelo e normalizando a intensidade dos pixels.
def carregarImagem(caminho):
imagem = image.load_img(caminho, target_size=(224, 224))
imagem = image.img_to_array(imagem)
imagem = np.expand_dims(imagem, axis = 0)
imagem /= 255
return imagem
Para cada imagem de testes já separadas no diretório testes, vamos padronizá-la.
caminho_imagem_1 = "testes/teste-1.jpg"
imagem_1_pad = carregarImagem(caminho_imagem_1)
Executar a inferência para obter sua predição.
predicao_1 = modelo_idade.predict(imagem_1_pad)
E, finalmente, somar as classes para a classificação mais exata.
idade_agrupada_1 = np.round(np.sum(predicao_1 * classes_idade_saida, axis = 1))
A função abaixo é para exibirmos no gráfico. Como elas estão com tamanhos diferentes, vamos também redimensioná-las para o padrão do moodeo, apenas por convenção, poderia ser outro tamanho.
imagem_1 = image.load_img(caminho_imagem_1, target_size=(224, 224))
Agora, faremos para todas as outras imagens.
caminho_imagem_2 = "testes/teste-2.png"
imagem_2_pad = carregarImagem(caminho_imagem_2)
imagem_2 = image.load_img(caminho_imagem_2, target_size=(224, 224))
predicao_2 = modelo_idade.predict(imagem_2_pad)
idade_agrupada_2 = np.round(np.sum(predicao_2 * classes_idade_saida, axis = 1))
caminho_imagem_3 = "testes/teste-3.png"
imagem_3_pad = carregarImagem(caminho_imagem_3)
imagem_3 = image.load_img(caminho_imagem_3, target_size=(224, 224))
predicao_3 = modelo_idade.predict(imagem_3_pad)
idade_agrupada_3 = np.round(np.sum(predicao_3 * classes_idade_saida, axis = 1))
caminho_imagem_4 = "testes/teste-4.png"
imagem_4_pad = carregarImagem(caminho_imagem_4)
imagem_4 = image.load_img(caminho_imagem_4, target_size=(224, 224))
predicao_4 = modelo_idade.predict(imagem_4_pad)
idade_agrupada_4 = np.round(np.sum(predicao_4 * classes_idade_saida, axis = 1))
caminho_imagem_5 = "testes/teste-5.png"
imagem_5_pad = carregarImagem(caminho_imagem_5)
imagem_5 = image.load_img(caminho_imagem_5, target_size=(224, 224))
predicao_5 = modelo_idade.predict(imagem_5_pad)
idade_agrupada_5 = np.round(np.sum(predicao_5 * classes_idade_saida, axis = 1))
caminho_imagem_6 = "testes/teste-6.png"
imagem_6_pad = carregarImagem(caminho_imagem_6)
imagem_6 = image.load_img(caminho_imagem_6, target_size=(224, 224))
predicao_6 = modelo_idade.predict(imagem_6_pad)
idade_agrupada_6 = np.round(np.sum(predicao_6 * classes_idade_saida, axis = 1))
Exibindo as imagens.
O resultado da idade é índice 0 do objeto idade_agrupada.
plt.figure(figsize=(20,10))
plt.subplot(231)
plt.title("Idade inferida " + str(idade_agrupada_1[0]))
plt.imshow(imagem_1)
plt.subplot(232)
plt.title("Idade inferida " + str(idade_agrupada_2[0]))
plt.imshow(imagem_2)
plt.subplot(233)
plt.title("Idade inferida " + str(idade_agrupada_3[0]))
plt.imshow(imagem_3)
plt.subplot(234)
plt.title("Idade inferida " + str(idade_agrupada_4[0]))
plt.imshow(imagem_4)
plt.subplot(235)
plt.title("Idade inferida " + str(idade_agrupada_5[0]))
plt.imshow(imagem_5)
plt.subplot(236)
plt.title("Idade inferida " + str(idade_agrupada_6[0]))
plt.imshow(imagem_6)
plt.show()
Analise se as idades estão coerentes com as imagens. Se estiver muito fora, avalie aumentar o número de épocas, por exemplo.
Agora vamos implementar a função que retornará a idade a partir de uma imagem de entrada já padronizada (assuma que foi padronizada por uma função como a carregarImagem. Assegure que a função retorne o valor da idade inferida em valor numérico, sem valor fracionário, somente inteiro.
def predizerIdade(imagem):
predicao = modelo_idade.predict(imagem)
idade_agrupada = np.round(np.sum(predicao * classes_idade_saida, axis = 1))
return round(int(idade_agrupada))
print(predizerIdade(imagem_6_pad))
A base utilizada será a mesma que aplicamos no modeo de idade. Nesse o caso o modelo terá uma tarefa mais fácil, pois ao invés de aproximidamente 100 classes vamos ter somente 2 classes, uma cada definir o gênero masculino e outra para o gênero feminino.
Vamos começar definindo o número de classes.
# IMPLEMENTAR
num_classes_genero = 2
Transformando os valores do conjunto de dados em one hot enconding para definição dos gêneros.
generos = df['gender'].values
generos_classes = keras.utils.to_categorical(generos, num_classes_genero)
Como para ambos os modelos, de idade e gênero, utilizamos a mesma base de transfer learning do VGGFace, vamos reutilizá-la também neste modelo.
for layer in modelo.layers[:-7]:
layer.trainable = False
saida_modelo = Sequential()
saida_modelo = Convolution2D(num_classes_genero, (1, 1), name='predictions')(modelo.layers[-4].output)
saida_modelo = Flatten()(saida_modelo)
saida_modelo = Activation('softmax')(saida_modelo)
modelo_genero = Model(inputs=modelo.input, outputs=saida_modelo)
É recomendavel manter a mesma porcentagem do modelo anterior para dividir os dados de treinamento e validação.
#IMPLEMENTAR
porcentagem_validacao = 0.2
treinamento_x, teste_x, treinamento_y, teste_y = train_test_split(features_imagem,
generos_classes,
test_size=porcentagem_validacao)
A seguir iremos treinar o modelo para classificação de gêneros. De forma semelhante realizada anteriormente, é necessário definir o número de épocas para esta etapa. Recomendamos um valor mínimo de 2.
# IMPLEMENTAR
numero_epocas = 2
Caso o modelo trave no treinamento, experimente diminuir o batch_size.
Este modelo é mais simples por ter menos classes, logo podemos assumir um batch_size maior que o anterior.
modelo_genero.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
historico_genero = modelo_genero.fit(x=treinamento_x, y=treinamento_y,
validation_data=(teste_x, teste_y), epochs=numero_epocas, batch_size=64)
Verifique abaixo, por meio dos gráficos, como está a evolução da função de erro e acurácia do modelo.
# Para deixar no formato do Seaborn os gráficos do Pyplot
sns.set()
# Exibindo dados de Acurácia/Precisão
plt.plot(historico_genero.history['acc'])
plt.plot(historico_genero.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# Exibindo dados de Perda
plt.plot(historico_genero.history['loss'])
plt.plot(historico_genero.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
Pergunta: Qual foi a tendência da função de erro e acurácia do modelo?
Resposta:
Salve os modelos abaixo para uso em inferências mais adiante no projeto.
# Salvando o modelo no formato HDf5
modelo_genero.save("modelos/modelo_genero.h5")
modelo_genero.save_weights("pesos/modelo_genero_pesos.h5")
Os comandos a seguir são para carregar os pesos e o modelo previamente treinado.
# carregando o modelo no formato HDf5
modelo_genero = load_model("modelos/modelo_genero.h5")
modelo_genero.load_weights("pesos/modelo_genero_pesos.h5")
Critério de sucesso: acurácia do modelo, com base nas amostra de teste, superior a 95%.
Com base em algumas amostras da base de testes, vamos inferir 6 imagens para verificarmos o desempenho do modeo. Selecionamos 3 imagens de homens e mulheres.
Como este modelo é mais simples, ou seja, o resultado sempre vai ser a classe com maior probabilidade, não precisamos realizar a somatória dos pesos individualmente como fizemos no modelo anterior.
Assim o resultado final será obtido pela maior classe. A função np.argmax retorna a maior classe da predição realizada pelo modelo.
De acordo com o encoding, o índice 0 indica gênero masculino e o índice 1 o gênero feminino.
predicao_1 = modelo_genero.predict(imagem_1_pad)
resultado_1 = "Masculino" if np.argmax(predicao_1) == 1 else "Feminino"
predicao_2 = modelo_genero.predict(imagem_2_pad)
resultado_2 = "Masculino" if np.argmax(predicao_2) == 1 else "Feminino"
predicao_3 = modelo_genero.predict(imagem_3_pad)
resultado_3 = "Masculino" if np.argmax(predicao_3) == 1 else "Feminino"
predicao_4 = modelo_genero.predict(imagem_4_pad)
resultado_4 = "Masculino" if np.argmax(predicao_4) == 1 else "Feminino"
predicao_5 = modelo_genero.predict(imagem_5_pad)
resultado_5 = "Masculino" if np.argmax(predicao_5) == 1 else "Feminino"
predicao_6 = modelo_genero.predict(imagem_6_pad)
resultado_6 = "Masculino" if np.argmax(predicao_6) == 1 else "Feminino"
Agora vamos exibir no gráfico as imagens e as inferências realizadas.
# Para deixar no formato do Seaborn os gráficos do Pyplot
sns.set()
sns.set_style("whitegrid", {'axes.grid' : False})
plt.figure(figsize=(20,10))
plt.subplot(231)
plt.title("Gênero inferido: " + resultado_1)
plt.imshow(imagem_1)
plt.subplot(232)
plt.title("Gênero inferido: " + resultado_2)
plt.imshow(imagem_2)
plt.subplot(233)
plt.title("Gênero inferido: " + resultado_3)
plt.imshow(imagem_3)
plt.subplot(234)
plt.title("Gênero inferido: " + resultado_4)
plt.imshow(imagem_4)
plt.subplot(235)
plt.title("Gênero inferido: " + resultado_5)
plt.imshow(imagem_5)
plt.subplot(236)
plt.title("Gênero inferido: " + resultado_6)
plt.imshow(imagem_6)
plt.show()
Vamos aproveitar para deixar preparado a função de predição de gênero para usarmos mais adiante. Implemente a função abaixo para retornar "Masculino" se a classe com maior probabilidade for 1, ou "Feminino" se a maior probabilidade for a classe igual a 0. É do mesmo jeito que fizemos anteriormente na predição de idade. Assuma que o parâmetro de entrada seja uma imagem já padronizada.
def predizerGenero(imagem):
predicao = modelo_genero.predict(imagem)
resultado = "Masculino" if np.argmax(predicao) == 1 else "Feminino"
return resultado
print(predizerGenero(imagem_6_pad))
Nesta etapa precisamos construir uma forma de extrair regiões de interesse de imagens que sejam rostos. Problema de reconhecimento de faces aprofundado é realizada a partir de um recorte de região de interesse. Logo, nossa estratégia agora é como extrair uma ou mais faces de uma imagem para que seja possível, posteriormente, aplicarmos os modelos que acabamos de classificar.
Vamos começar com um teste simples.
imagem = cv2.imread('testes/teste-8.png')
imagem = cv2.cvtColor(imagem, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(20,10))
plt.imshow(imagem)
plt.title("Pessoas")
Escolha uma forma de identificar rostos de uma imagem. Recomendamos as seguintes formas:
Dada a imagem de testes apresentada, execute seu algoritmo escolhido e retorne o número de faces na variável faces.
# IMPLEMENTAR
imagem = cv2.imread('testes/teste-8.png')
classificador_68_path = "modelos/shape_predictor_68_face_landmarks.dat"
classificador = dlib.shape_predictor(classificador_68_path)
detector = dlib.get_frontal_face_detector()
def obter_marcos(im):
retangulos = detector(im, 1)
if len(retangulos) == 0:
return None
marcos = []
for retangulo in retangulos:
marcos.append(np.matrix([[p.x, p.y] for p in classificador(im, retangulo).parts()]))
return marcos, retangulos
faces, retangulos = obter_marcos(imagem)
print("Faces encontradas: " + str(len(faces)))
Critério de sucesso: encontrar as 2 faces da imagem.
Com as faces identificadas, desenhe um retângulo em cada uma delas. Para desenhar um retângulo, é necessário os seguintes dados: coordenada x, y, comprimento (w) e altura (h).
Caso utilize o detector de faces do DLib, o valor da coordenada x é equivalente ao atributo left e a coordenada y é o equivalente ao atributo top.
O DLib é um identificador tão versátil que é capaz de identificar rostos parciais em uma imagem. No entanto, quando isso ocorre as coordenadas podem ser negativas, o que pode demandar um certo ajuste para obter o valor da região de interesse, por exemplo igualando a coordenada a 0.
Utilize o seguinte comando para desenhar um retângulo em cada face identificada.
cv2.rectangle(imagem_anotada, (x,y), (x+w,y+h), (255,255,0), 2)
Criando uma cópia da imagem original para anotação.
imagem_anotada = imagem.copy()
#IMPLEMENTAR
detector = dlib.get_frontal_face_detector()
def anotar_marcos(im, marcos, retangulos):
im = im.copy()
if marcos is None:
return im
for marco in marcos:
for k, d in enumerate(retangulos):
cv2.rectangle(im, (d.left(), d.top()), (d.right(), d.bottom()), (255, 0, 255), 2)
for idx, ponto in enumerate(marcos):
centro = (ponto[0, 0], ponto[0, 1])
cv2.circle(im, centro, 5, color=(0, 255, 0))
return im
imagem_marcos = anotar_marcos(imagem, faces, retangulos)
imagem_anotada = cv2.cvtColor(imagem_marcos, cv2.COLOR_BGR2RGB)
Exibindo a imagem com os retângulos desenhados.
plt.figure(figsize=(20,10))
plt.imshow(imagem_anotada)
plt.title("Pessoas")
Uma vez que já foi definido o melhor algoritmo de identificação de faces, precisamos construir uma função que posteriormente utilizaremos no processo de processamento de vídeos.
Essa função deverá receber na entrada uma imagem colorida e retornar, na forma de lista, um dicionário com as seguintes informações: extração do ROI do rosto e as coordenadas da localização do rosto.
Exemplo de retorno:
[{'coordenadas': [array([589, 290, 386, 386])],
'rosto': array([[[38, 21, 19],
[40, 21, 20],
[41, 22, 20],
...,
[36, 19, 13],
[37, 20, 15],
[40, 23, 18]],...}]
O atributo rosto utilizaremos para aplicarmos os modelos de reconhecimento de idade e gênero, e as coordenadas serão utilizadas para fazer os recortes nas imagens.
Com isso, a chave rosto deverá ter o slice da imagem neste padrão imagem[y:y+h, x:x+w] e a chave coordenadas deverá ter a saída dos pontos (x, y, h, w) neste padrão [np.array(lista_coordenadas)].
def obterFaces(im):
rostos = []
detector = dlib.get_frontal_face_detector()
retangulos = detector(im, 1)
def check_zero(coord):
if coord < 0:
return 0
return coord
for i, d in enumerate(retangulos):
x = check_zero(d.left())
y = check_zero(d.top())
w = check_zero(d.right() - d.left())
h = check_zero(d.bottom() - d.top())
rostos.append({
'coordenadas': [np.array([x, y, h, w])],
'rosto': im[y:y+h, x:x+w]
})
return rostos
Vamos aplicar um teste com a imagem anterior (variável imagem).
rostos = obterFaces(imagem)
rostos
Critério de sucesso: retornar 2 valores (da lista) com coordenadas e rosto para cada face identificada. Exemplo:
[{'coordenadas': [array([534, 20, 90, 100], dtype=int32)],
'rosto': array([...], shape=(0, 89, 3), dtype=uint8)},
{'coordenadas': [array([222, 13, 90, 100], dtype=int32)],
'rosto': array([...], shape=(0, 89, 3), dtype=uint8)}]
Agora vamos testar se os parâmetros estão funcionando adequadamente.
Primeiro a imagem do rosto.
sns.set_style("whitegrid", {'axes.grid' : False})
plt.figure(figsize=(20,10))
plt.imshow(rostos[0]["rosto"])
plt.title("Pessoas")
O próximo passo são as coordenadas. Para simplificar, vamos desenhar um retângulo na imagem original com estes pontos.
imagem_anotada = imagem.copy()
for (x,y,w,h) in rostos[0]["coordenadas"]:
cv2.rectangle(imagem_anotada, (x,y), (x+w,y+h), (255,255,0), 2)
sns.set_style("whitegrid", {'axes.grid' : False})
plt.figure(figsize=(20,10))
plt.imshow(imagem_anotada)
plt.title("Pessoas")
Adiante, quando aplicarmos a classificação dos rostos, precisaremos antes padronizar a imagem para o tamanho que o modelo recebe na camada de entrada bem como a normalização dos pixels da imagem, ou seja, ao invés de estar no formato de intensidade de pixel de 0 a 255, deve estar representado entre 0 e 1.
As implementações deverão ser as seguintes:
IMPLEMENTAR 1: redimensionar a imagem para o tamanho da camada de entrada. Utilize uma interpolação adequada para não perder qualidade de imagem.IMPLEMENTAR 2: normalizar a intensidade de pixel da imagem para 0 e 1 e não 0 a 255, que é o padrão.As demais instruções seguem inalteradas.
def padronizarROI(imagem):
# IMPLEMENTAR 1
imagem = cv2.resize(imagem, (224, 224), interpolation=cv2.INTER_LANCZOS4)
imagem = image.img_to_array(imagem)
imagem = np.expand_dims(imagem, axis = 0)
# IMPLEMENTAR 2
imagem /= 255
return imagem
Agora, vamos avançar para a identificação de objetos.
Além de identificarmos as pessoas, precisamos também identificar diferentes objetos. Uma forma de alcançarmos tal objetivo é utilizar um modelo já treinado com diversos objetos treinados.
O modolo Yolov3, por exemplo, possui 80 diferentes objetos em seu modelo, servindo muito bem para o propósito do desafio.
Primeiramente baixe os pesos diretamente no site do Darknet, neste link. Copie o arquivo yolov3.weights para a pasta pesos.
Confira o arquivo yolo-classes/coco.anmes. Estas são as classes de todos os objetos que são possíveis identificar.
Por fim, verifique se o arquivo config/yolov3.cfg está presente. Não é necessário ajustar nenhum parâmetro nele.
# Carregar os labels do conjunto de dados Coco
label_path = "yolo-classes/coco.names"
labels = open(label_path).read().strip().split("\n")
# Atribuir a cada label uma cor diferente (randômica)
np.random.seed(42)
cores = np.random.randint(0, 255, size=(len(labels), 3), dtype="uint8")
# Definir caminho dos arquivos de pesos e configuração
pesos_path ="pesos/yolov3.weights"
config_path = "config/yolov3.cfg"
# Carregar a rede
net = cv2.dnn.readNetFromDarknet(config_path, pesos_path)
Se o código carregou as configurações sem erros, estamos prontos para avançar.
Os valores de confiança e supressão não máxima são atributes importantes para o processo de detecção de objetos. Escolha valores adequados que permitam a detecção aceitável e com valores próximos os critérios de sucesso apresentandos mais adiante.
Os valores precisam ser numéricos fracionários. Sendo que 1 é igual a 100%. Valores médios costumam apresentar performance razoável.
conf_threshold = 0.5
nms_threshold = 0.5
Pergunta: Qual é a influência do parâmetro de confiança e supressão não máxima na performance do modelo?
Resposta: Confiança: Quanto maior o valor, menor a detecção de objetos de uma imagem, deixando um valor muito baixo o algoritmo começa a detectar varias coisas adicionais, gerando falsos positivos, com o valor muito alto o algoritmo detecta poucas faces, gerando falsos negativos.
Supressão não maxima: é responsavel por evitar que o mesmo objeto seja identificado varias vezes na mesma imagem.
De forma semelhante que foi feito na identificação de faces, vamos fazer para a identificação de objetos. Na função abaixo vamos implementar um algoritmo que retornará em um dicionário os valores do recorte da imagem, o que chamamos de região de interesse e suas coordenadas x, y, w e h.
A entrada da função é uma imagem colorida e uma lista de objetos para identificar, no formato de lista. Sua saída, é uma lista de objetos no seguinte formato:
[{'coordenadas': [array([589, 290, 386, 386])],
'objeto': array([[[38, 21, 19],
[40, 21, 20],
[41, 22, 20],
...,
[36, 19, 13],
[37, 20, 15],
[40, 23, 18]],...}]
Onde coordenadas é uma lista das coordenadas x, y, w e h no formato array, igual ao que foi apresentado anteriormente: [np.array(lista_coordenadas)]. E objeto é a região de interesse do objeto extraído, igual ao que foi apresentado anteriormente: imagem[y:y+h, x:x+w].
def obter_objetos(imagem, lista_objetos):
(H, W) = imagem.shape[:2]
ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]
blob = cv2.dnn.blobFromImage(imagem, 1/255.0, (416, 416), swapRB=True, crop=False)
net.setInput(blob)
layerOutputs = net.forward(ln)
boxes = []
confidences = []
classIDs = []
objetos = []
for output in layerOutputs:
for detection in output:
scores = detection[5:]
classID = np.argmax(scores)
confidence = scores[classID]
if confidence > conf_threshold:
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")
x = int(centerX - (width/2))
y = int(centerY - (height/2))
boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
classIDs.append(classID)
idxs = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
if len(idxs) > 0:
for i in idxs.flatten():
(x, y) = (boxes[i][0], boxes[i][1])
(w, h) = (boxes[i][2], boxes[i][3])
if x < 0:
x = 0
if y < 0:
y = 0
text = "{}: {:.4f}".format(labels[classIDs[i]], confidences[i])
if labels[classIDs[i]] in lista_objetos:
print("Identificado " + text)
# IMPLEMENTAR
item = {
"objeto": imagem[y:y+h, x:x+w],
"coordenadas": [np.array([x, y, w, h])]
}
objetos.append(item)
return objetos
Vamos utilizar outra imagem de testes para validar o algoritmo.
imagem_inferencia = cv2.imread("testes/teste-9.png")
imagem_inferencia = cv2.cvtColor(imagem_inferencia, cv2.COLOR_BGR2RGB)
Na variável lista_objetos preecha com uma lista de Strings com os valores "pessoa" e "gravata" que são os objetos a serem identificados.
# IMPLEMENTAR
lista_objetos = ["pessoa", "gravata"]
Execute a função com a lista definida.
objetos = obter_objetos(imagem_inferencia, lista_objetos)
Para nos certificamos que a identificação está correta, vamos desenhar um retângulo delimitador na imagem e verificar como foi a identificação.
Neste caso vamos utilizar o parâmetro coordenadas do retorno da função para cada objeto identificado.
imagem_anotada = imagem_inferencia.copy()
for obj in objetos:
for (x,y,w,h) in obj["coordenadas"]:
cv2.rectangle(imagem_anotada, (x,y), (x+w,y+h), (255,255,0), 2)
plt.figure(figsize=(20,10))
plt.imshow(imagem_anotada)
plt.title("Imagem Inferida")
Vamos dar uma olhada em 3 regiões de interesse detectadas. Lembrando que estas regiões são imagens, podemos renderizá-las diretamente no Pyplot.
plt.figure(figsize=(20,10))
plt.imshow(objetos[0]["objeto"])
plt.title("Região de interesse #1")
plt.figure(figsize=(20,10))
plt.imshow(objetos[1]["objeto"])
plt.title("Região de interesse #1")
plt.figure(figsize=(20,10))
plt.imshow(objetos[2]["objeto"])
plt.title("Região de interesse #1")
Perfeito, agora já estamos prontos para juntar todas as peças e começar nossa auditoria.
Esta é a parte final do projeto. Vamos fazer um checkpoint até aqui para ter certeza de que fizemos com sucesso os passos anteriores.
O que precisamos identificar no vídeo de auditoria:
Vamos começar definindo a lista de objetos.
lista_objetos = ["computador portátil", "celular", "teclado", "tv", "controle remoto"]
Nesta parte vamos reunir tudo o que fizemos até aqui.
O algoritmo irá abrir um vídeo e, frame a frame, analisar seu conteúdo.
Após a instrução if is_capturing iremos implementar as verificações.
A execução abaixo pode demorar, pois será analisado individualmente cada frame do vídeo.
O que precisamos fazer:
IMPLEMENTAR 1: obter as faces de uma imagem. Neste caso receberamos uma lista de rostos no padrão que já vimos, ou seja, uma lista de rosto e coordenadas.
IMPLEMENTAR 2: obter as a face de um item e padronizar. Lembre-se de utilizar a função de padronização que foi desenvolvida anteriormente, que tem por finalidade ajustar o tamanho da imagem e também normalizá-la.
IMPLEMENTAR 3: executar as funções de identificar a idade e gênero para a tomada de decisões de coleta de evidências.
IMPLEMENTAR 4: criar as regras de armazenamento de evidências para pessoas. Consulte as regras no início do projeto sobre quais os gêneros e idades de interesse. As regiões de interesse nesse caso, os rostos, deverão ser salvos individualmente na pasta resultado/homem e resultado/mulher.
IMPLEMENTAR 5: criar as regras de armazenamento de evidências para objetos. As regiões de interesse nesse caso, os objetos, deverão ser salvos individualmente na pasta resultado/objetos.
#cam.release()
cam = cv2.VideoCapture("videos/video-1.avi")
contador = 0
homem = 0
mulher = 0
outro = 0
try:
while(True):
contador += 1
is_capturing, imagem = cam.read()
if is_capturing:
# IMPLEMENTAR 1
# Obter Faces
faces = obterFaces(imagem)
for idx, face in enumerate(faces):
print("Encontrado " + str(len(faces)) + " rostos...")
# IMPLEMENTAR 2
# Padronizar a imagem do rosto (ROI)
# Obtenha a imagem do rosto da variável face e armazene em imagem_rosto
# Depois utilize a função padronizarROI, com a variável imagem_rosto para obter
# o rosto padronizado e armazenar em rosto_padronizado
imagem_rosto = face['rosto']
rosto_padronizado = padronizarROI(imagem_rosto)
# IMPLEMENTAR 3
# Chame as funções para predizer gênero e idade com a imagem padronizada do rosto
genero = predizerGenero(rosto_padronizado)
idade = predizerIdade(rosto_padronizado)
print(("Gênero: %s, idade: %s") % (str(genero), str(idade)))
# IMPLEMENTAR 4
# Estabeleça as regras de auditoria e salve as evidências (imagens) no diretório resultados
# de acordo com o identificação (resultado/homem, resultado/mulher)
# Cuidado para não sobrescrever as imagens
if genero == 'Masculino':
homem += 1
elif genero == 'Feminino':
mulher += 1
else:
outro += 1
today = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
if genero == 'Masculino' and idade >= 45:
cv2.imwrite("resultado/homem/homem_" + str(idade) + "_" + today + ".jpg", imagem_rosto)
elif genero == 'Feminino' and idade <= 45:
cv2.imwrite("resultado/mulher/mulher_" + str(idade) + "_" + today + ".jpg", imagem_rosto)
objetos = obter_objetos(imagem, lista_objetos)
# IMPLEMENTAR 5
# Estabeleça as regras de auditoria e salve as evidências (imagens) no diretório resultados
# de acordo com o identificação (resultado/objetos)
# Cuidado para não sobrescrever as imagens
if len(objetos) > 0:
for obj in objetos:
today = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
cv2.imwrite("resultado/objetos/obj" + today + ".jpg", obj['objeto'])
else:
break
except KeyboardInterrupt:
cam.release()
print("Interrompido")
print(mulher)
print(homem)
print(outros)
Critério de sucesso: identificação de 180 imagens de homens. Como uma mesma pessoa aparece em diferentes frames é comum repetir as imagens.
Critério de sucesso: identificação de 190 imagens de mulheres. Como uma mesma pessoa aparece em diferentes frames é comum repetir as imagens.
Critério de sucesso: identificação de 680 imagens de objetos selecionados. Como um mesmo objeto pode aparecer em diferentes frames é comum repetir as imagens
Com base nesta jornada de construção de modelos, análises de regiões de interesse e processamento de vídeo, comente quais seriam os principais pontos de melhoria para alcançar resultados melhores em todas as etapas.
Resposta:
a quantidade de épocas interfere diretamente no resultado, assim como a quantidade de dados do modelo, por limitações de hardware, acabei rodando com pouquissimas imagens (a maior parte do tempo somente 2 ou 5) e por isso obtive resultados bem baixos
Uma coisa que acredito que possa melhorar muito o resultado é a possibilidade de melhorar o dataset, pois existem muito mais imagens masculinas do que femininas, o que pode fazer o algoritmo ter uma assertividade menor ou até em casos extremos tendencias o modelo treinado para identificar melhor homens.
apenas uma observação... durante o processo de identificação de faces, não consegui fazer a identificação do rosto utilizando haar cascade, sendo necessário modificar para dlib e com isso identificar os 2 rostos com sucesso.
No ultimo exercicio o resultado foi pessimo, pois o algoritmo treinado ficou enviezado, identificando apenas mulheres de 50 anos, isso ocorreu pois a quantdade de imagens utilizada para treinar o algoritmo foi muito pequena, gerando um algoritmo com overtfit, com o devido treinamento a implementação deverá retornar o resultado esperado.